
在 Vue 3 中,Pinia 作為狀態管理庫,提供了靈活而強大的工具來管理應用的狀態。為了進一步提高 Pinia 的使用體驗,了解其底層機制和相關概念非常重要。本文將深入探討 Pinia 中的 effectScope、Map、WeakMap、Set、WeakSet 以及 private store 和 readonly store 的運作方式,並提供對應的實作範例。
effectScope 是 Vue 3 提供的一個響應式作用域控制機制,它允許我們在一個作用域中集中管理響應式狀態、計算屬性和監聽器,並能夠在需要時一鍵清除。Pinia 使用 effectScope 來管理 store 的響應式狀態,確保每個 store 的狀態在作用域被清理時自動停止追蹤。
import { shallowRef, effectScope } from 'vue';
// 創建一個新的 effectScope
const scope = effectScope();
scope.run(() => {
  const count = shallowRef<number>(0);
  
  // 使用 effectScope 管理的狀態
  const increment = () => {
    count.value++;
  };
  console.log('當前計數:', count.value);
  increment();
  console.log('增加後的計數:', count.value);
});
// 清理 scope 中的所有狀態
scope.stop();
在這個例子中,effectScope 管理了 count 和 increment 函數,當 scope.stop() 被調用時,所有在 scope 中的反應性依賴都會被清理。
在 Pinia 中,每個 store 都是由 effectScope 管理的,這意味著當我們銷毀 store 或不再使用時,可以通過停止該作用域來清理所有反應性狀態。這確保了 store 狀態的隔離和內存管理。
在 Pinia 的內部,Map 和 WeakMap 被用於管理 store 的依賴和作用域。這些數據結構的選擇使得 Pinia 能夠高效地處理狀態管理和依賴追蹤。
Map:用於存儲 key/value,key/value 都可以是任意類型。Pinia 使用 Map 來管理 store 實例和狀態。WeakMap:鍵必須是 Object,且其引用是弱引用。Pinia 使用 WeakMap 來處理與 store 相關的依賴,避免內存泄漏。Set 和 WeakSet:用於存儲唯一值,WeakSet 中的對象引用也是弱引用。Pinia 使用這些結構來管理動作和狀態追蹤。Map 和 WeakMap 的基本使用// 使用 Map 管理 store 的實例
interface CustomObject {
  count: number;
}
const storeMap = new Map<string, CustomObject>();
const myStore = { count: 0 };
storeMap.set('myStore', myStore);
console.log(storeMap.get('myStore')); //結果是 { count: 0 }
// 使用 WeakMap 管理 store 的依賴
interface MappingKey {
  id: string;
  state: string;
}
const storeWeakMap = new WeakMap<MappingKey, CustomObject>();
const storeInstanceKey: MappingKey = { 
  id: 'this is id',
  state: 'start...'
};
storeWeakMap.set(storeInstanceKey, { count: 1 });
console.log(storeWeakMap.get(storeInstance)); // { count: 1 }
private store 和 readonly store 的概念及實作關於我們在 Day8 提供單向數據流,揭開了 pinia 可維護性的正確寫法,但那只是冰山一角。接下來我們正是系統性地處理並解決可維護性的 pinia store 該如何展現。
Pinia 支持 private store 和 readonly store 的設計,使得我們可以創建只能在內部修改的狀態,並對外部提供只讀訪問。確保整體的設計是一致有系統性且嚴謹的
private store 的概念是指某些狀態只能在 store 內部修改,外部只能通過 actions 來操作。
(檔案src/stores/useCounterStore.ts)
import { computed, shallowRef } from 'vue'
import { defineStore, acceptHMRUpdate } from 'pinia';
// private pinia
const usePrivateCounterStore = defineStore("usePrivateCounterStore", () => {
  // private state::
  const count = shallowRef<number>(0);
  return {
    count,
  }
});
export const useCounterStore = defineStore('useCounterStore', () => {
  const privateCounterStore = usePrivateCounterStore();
  // state::
  // getter::
  const doubleCount = computed<number>(() => privateCounterStore.count * 2);
  // methods::
  const increment = (): void => {
    privateCounterStore.count++;
  };
  return {
    // state::
    count: computed<number>(() => privateCounterStore.count),
    // getters::
    doubleCount,
    // methods::
    increment
  }
});
if (import.meta.hot) {
  import.meta.hot.accept(acceptHMRUpdate(usePrivateCounterStore, import.meta.hot));
  import.meta.hot.accept(acceptHMRUpdate(useCounterStore, import.meta.hot));
}
在這個例子中,privateCount 是私有的,外部無法直接修改它,只能通過內部的 increment 方法來改變其值。可以更近一步確保封裝的安全,且不會受到外部 composables, stores, components 直接更改。
readonly store 允許我們定義一個僅提供讀取的 store 狀態,這對於需要保證狀態不可變的情況非常有用。
(檔案 src/stores/readonlyStore.ts)
import { defineStore } from 'pinia';
import { readonly, shallowRef } from 'vue';
export const useReadonlyStore = defineStore('readonly', () => {
  const count = shallowRef(0);
  const increment = () => {
    count.value++;
  };
  return { count: readonly(count), increment };
});
這裡的 count 是只讀的,雖然可以通過 increment 修改,但直接操作 count 的值將被禁止,這保護了狀態的完整性。
在使用 privateStore 的狀態下每次都要去產生兩個 store 一個去處理 private state 一個去定義 public method 或是 getters 的部分,這樣每次撰寫時都要重複做一件事,那我們把整個方法封裝起來,建立 privateState 的方法
(檔案 src/stores/privateState.ts)
import { UnwrapRef } from 'vue';
import { Router } from 'vue-router'
import { defineStore, StateTree, PiniaCustomStateProperties } from 'pinia';
export function definePrivateState<
  Id extends string,
  PrivateState extends StateTree,
  SetupReturn
>(
  id: Id, 
  privateStateFn: () => PrivateState, 
  setup: (privateState: UnwrapRef<PrivateState> & PiniaCustomStateProperties<PrivateState>, router: Router) => SetupReturn
 ) {
  const usePrivateState = defineStore(`${id}_private`, {
    state: privateStateFn,
  });
  return defineStore(id, () => {
    const privateState = usePrivateState();
    return setup(privateState.$state);
  });
}
這樣就可以這樣使用 private state pinia store
(檔案 src/stores/useNewCounterStore.ts)
import { computed } from "vue"
import { acceptHMRUpdate } from "pinia";
import { definePrivateState } from "./privateState";
import { RoutesStatus } from "../router";
export const useNewCounterStore = definePrivateState("useNewCounterStore", () => {
  return {
    count: 0,
  }
}, privateState => {
  const doubleCount = computed<number>(() => privateState.count * 2);
  const increment = (): void => {
    privateState.count++;
  };
  return {
    count: computed(() => privateState.count),
    doubleCount,
    increment,
  }
});
if (import.meta.hot) {
  import.meta.hot.accept(acceptHMRUpdate(useNewCounterStore, import.meta.hot));
}
Pinia 至高領域 - 自定義 pinia 的行為基本上有以上的操作會認為對 pinia 的理解已經非常通透了。
(圖片取自鬼滅之刃)
但要到達 pinia 至高領域,還需要可以自定義 pinia 的行為,但這些自定義行為需要道行高一點才能駕馭,就像炭治郎一開始要掌握日之呼吸不大可能的,建議以下深入的操作,在對 vue 和 pinia 有更深的理解再進行以下操作。(總共有 10 個型),由於篇幅有限,我只展示第一型最簡單的部分

這裡就魔改一下 privateState
(檔案 src/stores/privateState.ts)
import { UnwrapRef } from 'vue';
import { Router } from 'vue-router'
import { defineStore, StateTree, PiniaCustomStateProperties } from 'pinia';
export function definePrivateState<
  Id extends string,
  PrivateState extends StateTree,
  SetupReturn
>(
  id: Id, 
  privateStateFn: () => PrivateState, 
  setup: (privateState: UnwrapRef<PrivateState> & PiniaCustomStateProperties<PrivateState>, router: Router) => SetupReturn
 ) {
  const usePrivateState = defineStore(`${id}_private`, {
    state: privateStateFn,
    actions: {
      useRouter() {
        // 這裡注入 vue-router,讓往後使用的 store 可以直接呼叫 vue-router 的方法
        return this.router;
      }
    }
  });
  return defineStore(id, () => {
    const privateState = usePrivateState();
    return setup(privateState.$state, privateState.useRouter());
  });
}
為了做到上述的事情,要增加 vue-router 的 custom plugin 在 createPinia 的時候
(檔案 src/pinia/index.ts)
import { markRaw } from 'vue'
import { Router } from 'vue-router'
import { createPinia } from 'pinia';
import router from '../router';
export const pinia = createPinia();
pinia.use(({ store }) => {
  store.router = markRaw(router);
});
declare module 'pinia' {
  interface PiniaCustomProperties {
    router: Router,
  }
}
這時候在 main.ts 可以稍微修改
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { pinia } from './pinia' // 這裡改成這樣不用直接 createPinia
createApp(App)
  .use(router)
  .use(pinia) // 這裡修改
  .mount('#app')
上述完成後,就可以可以做一個 privateState 使用 router 做一些操作了,這裡我們複製原來的 useNewCounterStore,複製貼上製作一個新的 useNewBaseStore 作為展示
(檔案 src/stores/useNewBaseStore.ts)
import { computed } from "vue"
import { acceptHMRUpdate } from "pinia";
import { definePrivateState } from "./privateState";
import { RoutesStatus } from "../router";
export const useNewBaseStore = definePrivateState("useNewBaseStore", () => {
  return {
    count: 0,
  }
}, (privateState, router) => {
  const doubleCount = computed<number>(() => privateState.count * 2);
  const increment = (): void => {
    privateState.count++;
  };
  // 這裡即可直接呼叫 vue-router 進行陸游的操作,不用額外的 `const router = useRouter`
  const goToFoo = (): void => {
    router.push({ name: RoutesStatus.Foo });
  };
  return {
    count: computed(() => privateState.count),
    doubleCount,
    increment,
    goToFoo,
  }
});
if (import.meta.hot) {
  import.meta.hot.accept(acceptHMRUpdate(useNewBaseStore, import.meta.hot));
}
通過結合 TypeScript 與 Pinia,並深入了解 effectScope、依賴注入、Map、WeakMap、Set、WeakSet 以及 private store 和 readonly store 的概念,我們能夠更加靈活和高效地管理應用中的狀態。這些概念不僅提升了 Pinia 的可用性,也保證了應用的健壯性和可維護性。
希望這篇文章能幫助你更好地理解 Pinia 的底層原理和實際應用,讓你在開發過程中能夠更加得心應手!